package qj.tool.db.orientdb;

import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import qj.tool.db.Key;
import qj.util.Cols;
import qj.util.FileUtil;
import qj.util.StringUtil;
import qj.util.funct.Douce;
import qj.util.funct.F1;
import qj.util.funct.Fs;
import qj.util.funct.P0;
import qj.util.funct.P1;
import qj.util.funct.P2;

import com.orientechnologies.common.collection.OCompositeKey;
import com.orientechnologies.orient.core.db.document.ODatabaseDocumentTx;
import com.orientechnologies.orient.core.db.record.OIdentifiable;
import com.orientechnologies.orient.core.id.OClusterPositionFactory;
import com.orientechnologies.orient.core.id.ORID;
import com.orientechnologies.orient.core.id.ORecordId;
import com.orientechnologies.orient.core.index.OIndex;
import com.orientechnologies.orient.core.index.OIndex.IndexValuesResultListener;
import com.orientechnologies.orient.core.iterator.ORecordIteratorClass;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.core.sql.OCommandSQL;

@SuppressWarnings({ "rawtypes", "unchecked" })
public class DBImpl {
	public ODatabaseDocumentTx ds;
	public Converter converter = new Converter();
	HashMap<Class, P1> onCreateds = new HashMap<Class,P1>();
	HashMap<Class, P2> onUpdateds = new HashMap<Class,P2>();
	HashMap<Class, P1> onDeleteds = new HashMap<Class,P1>();
	HashMap<Class, P0> rebuildIndexs = new HashMap<Class, P0>();


	public DBImpl(ODatabaseDocumentTx ds) {
		this.ds = ds;
	}

	public <A> A getRecord(long index, Class<A> clazz) {

		int clusterId = ds.getMetadata().getSchema().getClass(Converter.entityType(clazz)).getClusterIds()[0];
		ORecordId id = new ORecordId(clusterId, OClusterPositionFactory.INSTANCE.valueOf(index));
		
		return converter.getConverter(clazz).eto((ODocument) ds.load(id));
	}
	

	// @Override
	public void saveAll(Collection<?> objects) {
		for (Object object : objects) {
			save(object);
		}
	}

	// @Override
	public void save(Object o) {
		Class<? extends Object> clazz = o.getClass();
		ClassConverter cc = converter.getConverter(clazz);
		
		KeyImpl key = cc.getKey(o);

		if (key == null) {
			ODocument document = new ODocument(Converter.entityType(clazz));
			cc.ote().e(o, document);
			document.save();
			cc.setKey(new KeyImpl(document.getIdentity()), o);
			
			// Listeners
			P1 onCreated = onCreateds.get(clazz);
			if (onCreated != null) {
				onCreated.e(o);
			}
		} else {
			ODocument document = (ODocument) ds.load(key.identity);
			
			P1 afterUpdate = beforeUpdate(document, clazz);
			cc.ote().e(o, document);
			document.save();
			
			afterUpdate.e(o);
		}
		
				
	}

	private P1 beforeUpdate(ODocument document, Class clazz) {
		final P2 onUpdated = onUpdateds.get(clazz);
		if (onUpdated == null) {
			return Fs.p1();
		}
		
		final Object before = converter.getConverter(clazz).eto(document);
		return new P1() {public void e(Object after) {
			onUpdated.e(before, after);
		}};
	}

	public void invoke(P0 p) {
		try {
			p.e();
		} finally {
			ds.close();
		}
	}

	public <A> A get(Class<A> clazz) {
		Iterator<ODocument> iter = ds.browseClass(Converter.entityType(clazz)).iterator();
		
		return !iter.hasNext() ? null : converter.getConverter(clazz).eto( iter.next() );
	}
	public <A> A get(Class<A> clazz, String field, Object value) {
		OIndex<?> index = ds.getMetadata().getIndexManager()
				.getIndex(Converter.entityType(clazz) + "." + field);

		if (value instanceof KeyImpl) {
			value = KeyImpl.toString((KeyImpl) value);
		}
		Object idO = index.get(value);
		
		if (idO == null) {
			return null;
		} else if (idO instanceof Collection) {
			throw new RuntimeException("More than 1 record is found: " + idO);
		} else {
			return get((ORecordId)idO, clazz);
		}
	}
	
	public <A> List<A> getAllMajor(Object value, boolean inclusive,
			Class<A> entityClass, String property) {
		OIndex<?> index = ds.getMetadata().getIndexManager().getIndex(Converter.entityType(entityClass) + "." + property);

		LinkedList<A> ret = new LinkedList<A>();
		ClassConverter<A> cc = converter.getConverter(entityClass);
		for (OIdentifiable id : index.getValuesMajor(value, inclusive)) {
			ret.add(cc.eto((ODocument) ds.load((ORID)id)));
		}
		return ret;
	}

	public <A> List<A> getAll1(Class<A> clazz, Collection<ORID> ids) {
		LinkedList<A> ret = new LinkedList<A>();
		ClassConverter<A> cc = converter.getConverter(clazz);
		for (ORID orid : ids) {
			ODocument doc = ds.load(orid);
			if (doc!=null) {
				// Sometimes the index fail
				ret.add(cc.eto(doc));
			}
		}
		return ret;
	}
	
	public <A> List<A> getAll(Class<A> clazz, Collection<Key> ids) {
		LinkedList<A> ret = new LinkedList<A>();
		for (Key id : ids) {
			ret.add(get(id, clazz));
		}
		return ret;
	}

//	@Deprecated
	public <A> List<A> getAll(Class<A> clazz) {
		LinkedList<A> ret = new LinkedList<A>();
		ClassConverter<A> cc = converter.getConverter(clazz);
		ORecordIteratorClass<ODocument> iter = ds.browseClass(Converter
				.entityType(clazz));
		for (Iterator iterator = iter; iterator.hasNext();) {
			ODocument doc = (ODocument) iterator.next();
			ret.add(cc.eto(doc));
		}
		return ret;
	}
	
	public <A> void each(Class<A> clazz, P1<A> p) {
		ClassConverter<A> cc = converter.getConverter(clazz);
		ORecordIteratorClass<ODocument> iter = ds.browseClass(Converter
				.entityType(clazz));
		for (Iterator iterator = iter; iterator.hasNext();) {
			ODocument doc = (ODocument) iterator.next();
			p.e(cc.eto(doc));
		}
	}
	
	public <A> List<A> getAll(Class<A> clazz, int maxFetch) {
		LinkedList<A> ret = new LinkedList<A>();
		ClassConverter<A> cc = converter.getConverter(clazz);
		ORecordIteratorClass<ODocument> iter = ds.browseClass(Converter
				.entityType(clazz));
		int count = 0;
		for (Iterator iterator = iter; iterator.hasNext();) {
			if (++count > maxFetch) {
				break;
			}
			ODocument doc = (ODocument) iterator.next();
			ret.add(cc.eto(doc));
		}
		return ret;
	}

	// @Override
	public <A> A get(Key key, Class<A> clazz) {
		if (key == null) {
			return null;
		}
		ORecordId id = ((KeyImpl) key).identity;
		return get(id, clazz);
	}
	
	public <A> F1<Key,A> getF(final Class<A> clazz) {
		return new F1<Key, A>() {public A e(Key key) {
			return get(key, clazz);
		}};
	}

	protected <A> A get(ORecordId id, Class<A> clazz) {
		ODocument doc = ds.load(id);
		if (doc == null) {
			return null;
		}
		ClassConverter<A> cc = converter.getConverter(clazz);
		return cc.eto(doc);
	}

	public void deleteAll(Class<?> clazz) {
		P1 onDeleted = onDeleteds.get(clazz);
		if (onDeleted==null) {
			for (ODocument doc : ds.browseClass(Converter.entityType(clazz))) {
				ds.delete(doc);
			}
		} else {
			ClassConverter<?> cc = converter.getConverter(clazz);
			for (ODocument doc : ds.browseClass(Converter.entityType(clazz))) {
				delete(cc.eto(doc));
			}
		}
	}
	public void deleteAll(List<Key> items) {
		// TODO invoke onDeleted
		for (Key key : items) {
			ds.delete(((KeyImpl)key).identity);
		}
	}
	


	// @Override
	public String serializeKey(Key key) {
		return serializeKey1(key);
	}
	
	public static String serializeKey1(Key key) {
		if (key == null) {
			return null;
		}

		return KeyImpl.toString(key);
	}

	public void begin() {
		ds.begin();
	}

	public void commit() {
		ds.commit();
	}

	public void rollback() {
		ds.rollback();
	}

	// @Override
	public <A> void delete(A o) {
		Class<? extends Object> clazz = o.getClass();
		ClassConverter cc = converter.getConverter(clazz);
		KeyImpl key = cc.getKey(o);
		delete(key);
		
		P1 onDeleted = onDeleteds.get(clazz);
		if (onDeleted != null) {
			onDeleted.e(o);
		}
		
	}

	// @Override
	public void delete(Key key) {
		// TODO invoke onDeleted
		ds.delete(((KeyImpl) key).identity);
	}

	public void updateIndex(String indexName, Object oldValue, Object newValue) {
		OIndex<?> index = ds.getMetadata().getIndexManager().getIndex(indexName);
		OIdentifiable id = (OIdentifiable) index.get(oldValue);
		index.remove(oldValue);
		index.put(newValue, id);
	}
	
	public boolean updateUniqueIndex(String indexName, Object oldValue, Object newValue) {
		OIndex<?> index = ds.getMetadata().getIndexManager().getIndex(indexName);
		if (index.contains(newValue)) {
			return false;
		}
		OIdentifiable id = (OIdentifiable) index.get(oldValue);
		index.remove(oldValue);
		index.put(newValue, id);
		return true;
	}

	public long count(Class<?> clazz) {
		return ds.getMetadata().getSchema().getClass(Converter.entityType(clazz)).count();		
	}
	

	public <A> List<A> getCompositeIndex(Class<A> clazz,
			String indexName, Object[] from, Object[] to) {
		return getCompositeIndex(clazz, indexName, from, to, -1);
	}
	public <A> List<A> getCompositeIndex(Class<A> clazz,
			String indexName, Object[] from, Object[] to, final int max) {
		final ClassConverter<A> cc = converter.getConverter(clazz);
		final LinkedList<A> ret = new LinkedList<A>();
		ds.getMetadata().getIndexManager().getIndex(indexName).getValuesBetween(compositeKey(from), true, compositeKey(to), true, new IndexValuesResultListener() {public boolean addResult(OIdentifiable orid) {
			ret.add(cc.eto((ODocument) ds.load((ORID) orid)));
			return max == -1 || ret.size() < max;
		}});
		return ret;
	}
	
	public <A> A getIndexLast(String indexName, Class<A> clazz) {
		Iterator<OIdentifiable> iter = ds.getMetadata().getIndexManager().getIndex(indexName).valuesInverseIterator();
		if (!iter.hasNext()) {
			return null;
		}
		return get((ORecordId) iter.next(), clazz);
	}
	
	public <A> A getCompositeIndex1(Class<A> clazz,
			String indexName, Object[] key) {
		OCompositeKey compositeKey = compositeKey(key);
		Collection<OIdentifiable> values = ds.getMetadata().getIndexManager().getIndex(indexName).getValuesBetween(compositeKey, true, compositeKey, true);
		ClassConverter<A> cc = converter.getConverter(clazz);
		
		for (OIdentifiable orid : values) {
			return cc.eto((ODocument) ds.load((ORID) orid));
		}
		return null;
	}

	public static OCompositeKey compositeKey(Object... parts) {
		LinkedList keys = new LinkedList();
		for (Object key : parts) {
			if (key instanceof KeyImpl) {
				keys.add(KeyImpl.toString((Key) key));
			} else if (key instanceof Date) {
				keys.add(((Date) key).getTime());
			} else {
				keys.add(key);
			}
		}
//		System.out.println("new OCompositeKey(keys)=" + new OCompositeKey(keys));
		return new OCompositeKey(keys);
	}


	public <A> A getDictionary(String name, String indexName, Class<A> clazz) {
		OIndex<?> index = ds.getMetadata().getIndexManager().getIndex(indexName);
		ClassConverter<A> cc = converter.getConverter(clazz);;
		Object value = index.get(name);
		if (value == null) {
			return null;
		}
		return value instanceof ORecordId ? 
				cc.eto((ODocument)((ORecordId) value).getRecord()) : 
				cc.eto((ODocument) value);
	}

	public <A> void setDictionary(String name, String indexName, A value) {
		OIndex<?> index = ds.getMetadata().getIndexManager().getIndex(indexName);
		ClassConverter<A> cc = (ClassConverter<A>) converter.getConverter(value.getClass());
		ODocument doc = new ODocument();
		cc.ote().e(value, doc);
		index.put(name, doc);
	}
	
	public <A> void deleteDictionary(String name, String indexName) {
		OIndex<?> index = ds.getMetadata().getIndexManager().getIndex(indexName);
		index.remove(name);
	}
	
	public <A> List<A> getColTime(String key, Date from, Date to, String colTimeIndexName, final Class<A> clazz) {
		return getCompositeIndex(clazz, "timeIndex", new Object[]{colTimeIndexName + "::" + key, from}, new Object[]{colTimeIndexName + "::" + key, to});
	}
	
	public <A> void addColTimeIndex(final String name, final Class<A> clazz,
			final F1<A, Collection<String>> colF, final F1<A, Date> timeF) {
		final OIndex<?> index = ds.getMetadata().getIndexManager().getIndex("timeIndex");
		final ClassConverter<A> cc = converter.getConverter(clazz);

		final P1<A> add = new P1<A>() {public void e(A a) {
			long time = timeF.e(a).getTime();
			ORID idObj = KeyImpl.idF.e(cc.getKey(a));
			for (String key : colF.e(a)) {
				index.put(new OCompositeKey(name + "::" + key, time), idObj);
			}
		}};
		final P1<A> remove = new P1<A>() {public void e(A a) {
			long time = timeF.e(a).getTime();
			ORID idObj = KeyImpl.idF.e(cc.getKey(a));
			for (String key : colF.e(a)) {
				index.remove(new OCompositeKey(name + "::" + key, time), idObj);
			}
		}};


		P0 rebuildIndex = new P0() {public void e() {
			for (ODocument doc : ds.browseClass(Converter.entityType(clazz))) {
				A a = cc.eto(doc);
				remove.e(a);
				add.e(a);
			}
		}};
		
		onCreated(clazz, add);
		
		onUpdated(clazz, new P2<A,A>() {public void e(A before, A after) {
			long time1 = timeF.e(before).getTime();
			long time2 = timeF.e(after).getTime();
			
			if (time1 == time2) { // For fast update
				Douce<Set<String>, Set<String>> douce = Cols.getDiffs(colF.e(before), colF.e(after));
				Set<String> beforeOnlyCol = douce.get1();
				Set<String> afterOnlyCol = douce.get1();
				
				for (String key : beforeOnlyCol) {
					index.remove(new OCompositeKey(name + "::" + key, time1));
				}
				
				ORID idObj = KeyImpl.idF.e(cc.getKey(before));
				for (String key : afterOnlyCol) {
					index.put(new OCompositeKey(name + "::" + key, time1), idObj);
				}
			} else {
				remove.e(before);
				add.e(before);
			}
		}});
		
		onDeleted(clazz, remove);

		rebuildIndexs.put(clazz, rebuildIndex);
	}
	
	public void rebuildIndex(Class clazz) {
		P0 p0 = rebuildIndexs.get(clazz);
		if (p0 != null) {
			p0.e();
		}
	}

	public <A> void onCreated(Class<A> clazz, P1<A> p1) {
		onCreateds.put(clazz, p1);
	}
	public <A> void onDeleted(Class<A> clazz, P1<A> p1) {
		onDeleteds.put(clazz, p1);
	}
	public <A> void onUpdated(Class<A> clazz, P2<A, A> p2) {
		onUpdateds.put(clazz, p2);
	}


	public void execFile(String filePath) {
		String content = FileUtil.readFileToString(filePath);
		exec(content);
	}

	public void exec(String content) {
		for (String command : content.split(";")) {
			command = command.trim();
			if (StringUtil.isBlank(command) || command.startsWith("//")) {
				continue;
			}
			
			try {
				ds.command(new OCommandSQL(command)).execute();
			} catch (RuntimeException e) {
				System.out.println("Error executing command: " + command);
				throw e;
			}
		}
	}

	
}
